/**********************************************************************

  Audacity: A Digital Audio Editor

  @file RealtimeEffectState.cpp

  Paul Licameli split from RealtimeEffectManager.cpp

 *********************************************************************/

#include "RealtimeEffectState.h"

#include "Channel.h"
#include "EffectInterface.h"
#include "MessageBuffer.h"
#include "PluginManager.h"
#include "SampleCount.h"

#include <chrono>
#include <thread>
#include <condition_variable>

//! Mediator of two-way inter-thread communication of changes of settings
class RealtimeEffectState::AccessState : public NonInterferingBase
{
public:
    AccessState(const EffectSettingsManager& effect, RealtimeEffectState& state)
        : mEffect{effect}
        , mState{state}
    {
        // Clean initial state of the counter
        state.mMainSettings.counter = 0;
        Initialize(state.mMainSettings.settings,
                   state.mMessage.get(), state.mMovedOutputs.get());
    }

    void Initialize(const EffectSettings& settings,
                    const EffectInstance::Message* pMessage,
                    const EffectOutputs* pOutputs)
    {
        mLastSettings = { settings, 0 };
        // Initialize each message buffer with two copies
        mChannelToMain.Write(ToMainSlot { { 0,
                                 pOutputs ? pOutputs->Clone() : nullptr } });
        mChannelToMain.Write(ToMainSlot { { 0,
                                 pOutputs ? pOutputs->Clone() : nullptr } });
        mChannelFromMain.Write(FromMainSlot { settings, pMessage });
        mChannelFromMain.Write(FromMainSlot { settings, pMessage });

        mMainThreadId = std::this_thread::get_id();
    }

    void MainRead()
    {
        mChannelToMain.Read<ToMainSlot::Reader>(mState.mMovedOutputs.get(),
                                                mCounter);
    }

    void MainWrite(SettingsAndCounter&& settings,
                   std::unique_ptr<EffectInstance::Message> pMessage)
    {
        // Main thread may simply swap new content into place
        mChannelFromMain.Write(FromMainSlot::Message {
            std::move(settings.settings), settings.counter, std::move(pMessage) });
    }

    void MainWrite(SettingsAndCounter::Counter counter,
                   std::unique_ptr<EffectInstance::Message> pMessage)
    {
        mChannelFromMain.Write(FromMainSlot::ShortMessage {
            counter, std::move(pMessage) });
    }

    struct CounterAndOutputs {
        Response::Counter counter;
        EffectOutputs* pOutputs{};
    };
    void WorkerRead()
    {
        // Worker thread avoids memory allocation.  It copies the contents of any
        mChannelFromMain.Read<FromMainSlot::Reader>(mEffect, mState);
    }

    void WorkerWrite()
    {
        {
            std::unique_lock lk(mLockForCV);

            // Worker thread avoids memory allocation.
            mChannelToMain.Write(CounterAndOutputs {
                mState.mWorkerSettings.counter, mState.mOutputs.get() });
        }
        mCV.notify_one();
    }

    struct ToMainSlot {
        // For initialization of the channel
        ToMainSlot() = default;
        explicit ToMainSlot(Response response)
            : mResponse{std::move(response)}
        {}
        ToMainSlot& operator=(ToMainSlot&&) = default;

        // Worker thread writes the slot
        ToMainSlot& operator=(CounterAndOutputs&& arg)
        {
            // This happens during MessageBuffer's busying of the slot
            mResponse.counter = arg.counter;
            if (mResponse.pOutputs && arg.pOutputs) {
                mResponse.pOutputs->Assign(std::move(*arg.pOutputs));
            }
            return *this;
        }

        struct Reader {
            Reader(ToMainSlot&& slot, EffectOutputs* pOutputs,
                   Response::Counter& counter)
            {
                // Main thread is not under the performance constraints of the
                // worker, but Assign is still used so that
                // members of underlying vectors or other containers do not
                // relocate
                if (pOutputs && slot.mResponse.pOutputs) {
                    pOutputs->Assign(std::move(*slot.mResponse.pOutputs));
                }
                counter = slot.mResponse.counter;
            }
        };

        Response mResponse;
    };

    struct FromMainSlot {
        struct Message : SettingsAndCounter {
            std::unique_ptr<EffectInstance::Message> pMessage;
        };
        struct ShortMessage {
            SettingsAndCounter::Counter counter;
            std::unique_ptr<EffectInstance::Message> pMessage;
        };

        // For initialization of the channel
        FromMainSlot() = default;
        explicit FromMainSlot(const EffectSettings& settings,
                              const EffectInstance::Message* pMessage)
        // Copy std::any
            : mMessage{settings, 0, pMessage ? pMessage->Clone() : nullptr}
        {}
        FromMainSlot& operator=(FromMainSlot&&) = default;

        // Main thread writes the slot
        FromMainSlot& operator=(Message&& message)
        {
            mMessage.SettingsAndCounter::swap(message);
            if (message.pMessage && mMessage.pMessage) {
                // Merge the incoming message with any still unconsumed message
                mMessage.pMessage->Merge(std::move(*message.pMessage));
            }
            return *this;
        }

        // Main thread writes the slot
        FromMainSlot& operator=(ShortMessage&& message)
        {
            mMessage.counter = message.counter;
            if (message.pMessage && mMessage.pMessage) {
                // Merge the incoming message with any still unconsumed message
                mMessage.pMessage->Merge(std::move(*message.pMessage));
            }
            return *this;
        }

        // Worker thread reads the slot
        struct Reader {
            Reader(FromMainSlot&& slot,
                   const EffectSettingsManager& effect, RealtimeEffectState& state)
            {
                auto& settings = state.mWorkerSettings;
                if (slot.mMessage.counter == settings.counter) {
                    return;//copy once
                }
                settings.counter = slot.mMessage.counter;

                // This happens during MessageBuffer's busying of the slot
                effect.CopySettingsContents(
                    slot.mMessage.settings, settings.settings);
                settings.settings.extra = slot.mMessage.settings.extra;
                if (slot.mMessage.pMessage && state.mMovedMessage) {
                    // Copy the message from the buffer (not a merge)
                    state.mMovedMessage->Assign(std::move(*slot.mMessage.pMessage));
                }
            }
        };

        Message mMessage;
    };

    const EffectSettingsManager& mEffect;
    RealtimeEffectState& mState;

    MessageBuffer<FromMainSlot> mChannelFromMain;
    Response::Counter mCounter;
    SettingsAndCounter mLastSettings;

    MessageBuffer<ToMainSlot> mChannelToMain;

    std::mutex mLockForCV;
    std::condition_variable mCV;

    std::thread::id mMainThreadId;
};

//! Main thread's interface to inter-thread communication of changes of settings
struct RealtimeEffectState::Access final : EffectSettingsAccess {
    Access() = default;
    explicit Access(RealtimeEffectState& state)
        : mwState{state.weak_from_this()}
    {
    }

    ~Access() override = default;

    const EffectSettings& Get() override
    {
        if (auto pState = mwState.lock()) {
            if (auto pAccessState = pState->GetAccessState()) {
                if (pAccessState->mState.mInitialized) {
                    // try once
                    assert(pAccessState->mState.mInitialized);
                    auto& lastSettings = pAccessState->mLastSettings;
                    // Assigns to mCounter
                    pAccessState->MainRead();
                } else {
                    // Not yet waiting on the other thread's progress
                    // Not necessarily values yet in the state's Settings objects
                }
                return pAccessState->mLastSettings.settings;
            }
        }
        // Non-modal dialog may have outlived the RealtimeEffectState
        static EffectSettings empty;
        return empty;
    }

    void Set(EffectSettings&& settings, std::unique_ptr<Message> pMessage)
    override
    {
        if (auto pState = mwState.lock()) {
            if (auto pAccessState = pState->GetAccessState()) {
                if (pMessage && !pAccessState->mState.mInitialized) {
                    // Other thread isn't processing.
                    // Let the instance consume the message directly.
                    if (auto pInstance = pState->mwInstance.lock()) {
                        auto& stateSettings = pState->mMainSettings.settings;
                        stateSettings = std::move(settings);
                        EffectInstance::MessagePackage package{
                            stateSettings, pMessage.get()
                        };
                        pInstance->RealtimeProcessStart(package);
                        pInstance->RealtimeProcessEnd(stateSettings);
                        pAccessState->mLastSettings.settings = stateSettings;
                        return;
                    }
                }
                auto& lastSettings = pAccessState->mLastSettings;
                // move to remember values here
                lastSettings.settings = std::move(settings);
                ++lastSettings.counter;
                // move a copy to there
                pAccessState->MainWrite(
                    SettingsAndCounter { lastSettings }, std::move(pMessage));
            }
        }
    }

    void Set(std::unique_ptr<Message> pMessage)
    override
    {
        if (auto pState = mwState.lock()) {
            if (auto pAccessState = pState->GetAccessState()) {
                if (pMessage && !pAccessState->mState.mInitialized) {
                    // Other thread isn't processing.
                    // Let the instance consume the message directly.
                    if (auto pInstance = pState->mwInstance.lock()) {
                        auto& stateSettings = pState->mMainSettings.settings;
                        EffectInstance::MessagePackage package{
                            stateSettings, pMessage.get()
                        };
                        pInstance->RealtimeProcessStart(package);
                        pInstance->RealtimeProcessEnd(stateSettings);
                        // Don't need to update pAccessState->mLastSettings
                        return;
                    }
                }
                auto& lastSettings = pAccessState->mLastSettings;
                // Don't update settings, but do count
                ++lastSettings.counter;
                pAccessState->MainWrite(
                    lastSettings.counter, std::move(pMessage));
            }
        }
    }

    void Flush() override
    {
        if (auto pState = mwState.lock()) {
            if (auto pAccessState = pState->GetAccessState()) {
                assert(pAccessState->mMainThreadId == std::this_thread::get_id());

                if (pAccessState->mState.mInitialized) {
                    std::unique_lock lk(pAccessState->mLockForCV);
                    pAccessState->mCV.wait(lk,
                                           [&] {
                        auto& lastSettings = pAccessState->mLastSettings;
                        pAccessState->MainRead();
                        return pAccessState->mCounter == lastSettings.counter;
                    }
                                           );
                }

                // Update what GetSettings() will return, during play and before
                // Finalize(), but after it is confirmed that any worker thread has
                // seen the values given to the last Set().  These values will also
                // be returned by Get().
                pState->mMainSettings.Set(pAccessState->mLastSettings);
            }
        }
    }

    bool IsSameAs(const EffectSettingsAccess& other) const override
    {
        if (auto pOther = dynamic_cast<const Access*>(&other)) {
            auto& mine = mwState;
            auto& theirs = pOther->mwState;
            auto less = std::owner_less{};
            return !(less(mine, theirs) || less(theirs, mine));
        }
        return false;
    }

    //! Store no state here but this weak pointer, so `IsSameAs` isn't lying
    std::weak_ptr<RealtimeEffectState> mwState;
};

RealtimeEffectState::RealtimeEffectState(const PluginID& id)
{
    SetID(id);
    BuildAll();
}

RealtimeEffectState& RealtimeEffectState::operator=(const RealtimeEffectState& other)
{
    assert(other.mID == mID);
    SetActive(other.IsActive());
    if (!mPlugin || !other.mPlugin) {
        return *this;
    }
    CommandParameters params;
    if (other.mPlugin->SaveSettings(mMainSettings.settings, params)) {
        mPlugin->LoadSettings(params, mMainSettings.settings);
    } else {
        assert(false);
    }
    return *this;
}

RealtimeEffectState::~RealtimeEffectState()
{
}

void RealtimeEffectState::SetID(const PluginID& id)
{
    bool empty = id.empty();
    if (mID.empty() && !empty) {
        mID = id;
        GetEffect();
    } else {
        // Set mID to non-empty at most once
        assert(empty);
    }
}

const PluginID& RealtimeEffectState::GetID() const noexcept
{
    return mID;
}

const EffectInstanceFactory* RealtimeEffectState::GetEffect()
{
    if (!mPlugin) {
        mPlugin = EffectFactory::Call(mID);
        if (mPlugin) {
            // Also make EffectSettings, but preserve activation
            auto wasActive = mMainSettings.settings.extra.GetActive();
            mMainSettings.counter = 0;
            mMainSettings.settings = mPlugin->MakeSettings();
            mMainSettings.settings.extra.SetActive(wasActive);
            mOutputs = mPlugin->MakeOutputs();
            mMovedOutputs = mPlugin->MakeOutputs();
        }
    }
    return mPlugin;
}

std::shared_ptr<EffectInstance> RealtimeEffectState::MakeInstance()
{
    mMovedMessage.reset();
    mMessage.reset();
    auto result = mPlugin->MakeInstance();
    if (result) {
        // Allocate presized containers in messages, so later
        // copies of contents might avoid free store operations
        mMessage = result->MakeMessage();
        mMovedMessage = result->MakeMessage();
        if (auto state = GetAccessState()) {
            state->Initialize(mMainSettings.settings,
                              mMessage.get(), mMovedOutputs.get());
        }
    }
    return result;
}

std::shared_ptr<EffectInstance>
RealtimeEffectState::EnsureInstance(double sampleRate, size_t audioThreadBufferSize)
{
    if (!mPlugin) {
        return {};
    }

    auto pInstance = mwInstance.lock();
    if (!mInitialized) {
        //! copying settings in the main thread while worker isn't yet running
        mWorkerSettings = mMainSettings;
        mLastActive = IsActive();

        //! If there was already an instance, recycle it; else make one here
        if (!pInstance) {
            mwInstance = pInstance = MakeInstance();
        }
        if (!pInstance) {
            return {};
        }

        // PRL: conserving pre-3.2.0 behavior, but I don't know why this arbitrary
        // number was important
        pInstance->SetBlockSize(512);

        if (!pInstance->RealtimeInitialize(mMainSettings.settings, sampleRate, audioThreadBufferSize)) {
            return {};
        }
        mInitialized = true;
        return pInstance;
    }
    return pInstance;
}

std::shared_ptr<EffectInstance> RealtimeEffectState::GetInstance()
{
    //! If there was already an instance, recycle it; else make one here
    auto pInstance = mwInstance.lock();
    if (!pInstance && mPlugin) {
        mwInstance = pInstance = MakeInstance();
    }
    return pInstance;
}

std::shared_ptr<EffectInstance>
RealtimeEffectState::Initialize(double sampleRate, size_t audioThreadBufferSize)
{
    if (!mPlugin) {
        return {};
    }

    mCurrentProcessor = 0;
    mGroups.clear();
    mLatency = {};
    return EnsureInstance(sampleRate, audioThreadBufferSize);
}

namespace {
// The caller passes the number of channels to process and specifies
// the number of input and output buffers.  There will always be the
// same number of output buffers as there are input buffers.
//
// Effects require a certain number of input and output buffers.
// The number of channels we're currently processing may mismatch
// the effect's requirements.  Allocate some inputs repeatedly to a processor
// that needs more, or allocate multiple processors if they accept too few.
// Continue until the output buffers are all allocated.
template<typename F>
void AllocateChannelsToProcessors(
    unsigned chans, const unsigned numAudioIn, const unsigned numAudioOut,
    const F& f)
{
    unsigned indx = 0;
    for (unsigned ondx = 0; ondx < chans; ondx += numAudioOut) {
        // Pass the function indices into the arrays of buffers
        if (!f(indx, ondx)) {
            return;
        }
        indx += numAudioIn;
        indx %= chans;
    }
}
}

//! Set up processors to be visited repeatedly in Process.
/*! The iteration over channels in AddGroup and Process must be the same */
std::shared_ptr<EffectInstance>
RealtimeEffectState::AddGroup(
    const ChannelGroup* group, unsigned chans, float sampleRate, size_t audioThreadBufferSize)
{
    auto pInstance = EnsureInstance(sampleRate, audioThreadBufferSize);
    if (!pInstance) {
        return {};
    }
    if (!mPlugin) {
        return {};
    }
    auto first = mCurrentProcessor;
    const auto numAudioIn = pInstance->GetAudioInCount();
    const auto numAudioOut = pInstance->GetAudioOutCount();
    AllocateChannelsToProcessors(chans, numAudioIn, numAudioOut,
                                 [&](unsigned, unsigned){
        // Add a NEW processor
        if (pInstance->RealtimeAddProcessor(
                mWorkerSettings.settings, mOutputs.get(), numAudioIn, sampleRate)
            ) {
            mCurrentProcessor++;
            return true;
        } else {
            return false;
        }
    });
    if (mCurrentProcessor > first) {
        // Remember the sampleRate of the group, so latency can be computed
        // later
        mGroups[group] = { first, sampleRate };
        return pInstance;
    }
    return {};
}

bool RealtimeEffectState::ProcessStart(bool running)
{
    // Get state changes from the main thread
    // Note that it is only here that the answer of IsActive() may be changed,
    // and it is important that for each state the answer is unchanging in one
    // processing scope.
    if (auto pAccessState = TestAccessState()) {
        pAccessState->WorkerRead();
    }

    // Detect transitions of activity state
    auto pInstance = mwInstance.lock();
    bool active = IsActive() && running;
    if (active != mLastActive) {
        if (pInstance) {
            bool success = active
                           ? pInstance->RealtimeResume()
                           : pInstance->RealtimeSuspend();
            if (!success) {
                return false;
            }
        }
        mLastActive = active;
    }

    bool result = false;
    if (pInstance) {
        // Consume messages even if not processing
        // (issue #3855: plain UI for VST 2 effects)

        // Assuming we are in a processing scope, use the worker settings
        EffectInstance::MessagePackage package{
            mWorkerSettings.settings, mMovedMessage.get()
        };
        result = pInstance->RealtimeProcessStart(package);
    }

    if (!pInstance || !active) {
        return false;
    } else {
        return result;
    }
}

#define stackAllocate(T, count) static_cast<T*>(alloca(count * sizeof(T)))

//! Visit the effect processors that were added in AddGroup
/*! The iteration over channels in AddGroup and Process must be the same */
size_t RealtimeEffectState::Process(
    const ChannelGroup* group, unsigned chans,
    const float* const* inbuf, float* const* outbuf, float* const dummybuf,
    size_t numSamples)
{
    const auto pInstance = mwInstance.lock();
    const auto& pair = mGroups[group];
    const float** const clientIn
        =pInstance ? stackAllocate(const float*, pInstance->GetAudioInCount())
          : nullptr;

    const auto PointAtCorrectInputBuffers
        =[&](unsigned numAudioIn, unsigned indx) {
        // Point at the correct input buffers
        unsigned copied = std::min(chans - indx, numAudioIn);
        std::copy(inbuf + indx, inbuf + indx + copied, clientIn);
        // If there are too few input channels for what the processor requires,
        // re-use input channels from the beginning
        while (auto need = numAudioIn - copied)
        {
            auto moreCopied = std::min(chans, need);
            std::copy(inbuf, inbuf + moreCopied, clientIn + copied);
            copied += moreCopied;
        }
    };

    if (!mPlugin || !pInstance || !mLastActive) {
        // Process trivially
        for (size_t ii = 0; ii < chans; ++ii) {
            memcpy(outbuf[ii], inbuf[ii], numSamples * sizeof(float));
        }
        if (pInstance) {
            auto processor = pair.first;
            const auto numAudioIn = pInstance->GetAudioInCount();
            const auto numAudioOut = pInstance->GetAudioOutCount();
            AllocateChannelsToProcessors(
                chans, numAudioIn, numAudioOut, [&](unsigned indx, unsigned ondx) {
                PointAtCorrectInputBuffers(numAudioIn, indx);

                // Inner loop over blocks
                const auto blockSize = pInstance->GetBlockSize();
                for (size_t block = 0; block < numSamples; block += blockSize) {
                    auto cnt = std::min(numSamples - block, blockSize);
                    pInstance->RealtimePassThrough(
                        processor, mWorkerSettings.settings, clientIn, cnt);
                    for (size_t i = 0; i < numAudioIn; i++) {
                        if (clientIn[i]) {
                            clientIn[i] += cnt;
                        }
                    }
                }
                ++processor;
                return true;
            });
        }
        return 0;
    }
    const auto numAudioIn = pInstance->GetAudioInCount();
    const auto numAudioOut = pInstance->GetAudioOutCount();
    const auto clientOut = stackAllocate(float*, numAudioOut);
    size_t len = 0;
    auto processor = pair.first;
    // Outer loop over processors
    AllocateChannelsToProcessors(
        chans, numAudioIn, numAudioOut, [&](unsigned indx, unsigned ondx) {
        PointAtCorrectInputBuffers(numAudioIn, indx);

        // Point at the correct output buffers
        unsigned copied = std::min(chans - ondx, numAudioOut);
        std::copy(outbuf + ondx, outbuf + ondx + copied, clientOut);
        if (copied < numAudioOut) {
            // Make determinate pointers
            std::fill(clientOut + copied, clientOut + numAudioOut, dummybuf);
        }

        // Inner loop over blocks
        const auto blockSize = pInstance->GetBlockSize();
        for (size_t block = 0; block < numSamples; block += blockSize) {
            auto cnt = std::min(numSamples - block, blockSize);
            // Assuming we are in a processing scope, use the worker settings
            auto processed = pInstance->RealtimeProcess(
                processor, mWorkerSettings.settings, clientIn, clientOut, cnt);
            if (!mLatency) {
                // Find latency once only per initialization scope,
                // after processing one block
                mLatency.emplace(
                    pInstance->GetLatency(mWorkerSettings.settings, pair.second));
            }
            for (size_t i = 0; i < numAudioIn; i++) {
                if (clientIn[i]) {
                    clientIn[i] += cnt;
                }
            }
            for (size_t i = 0; i < numAudioOut; i++) {
                if (clientOut[i]) {
                    clientOut[i] += cnt;
                }
            }
            if (ondx == 0) {
                // For the first processor only
                len += processed;
                auto discard = limitSampleBufferSize(len, *mLatency);
                len -= discard;
                *mLatency -= discard;
            }
        }
        ++processor;
        return true;
    });
    // Report the number discardable during the processing scope
    // We are assuming len as calculated above is the same in case of multiple
    // processors
    return numSamples - len;
}

bool RealtimeEffectState::ProcessEnd()
{
    auto pInstance = mwInstance.lock();
    bool result = pInstance
                  &&// Assuming we are in a processing scope, use the worker settings
                  pInstance->RealtimeProcessEnd(mWorkerSettings.settings)
                  && IsActive() && mLastActive;

    if (auto pAccessState = TestAccessState()) {
        // Always done, regardless of activity
        // Some dialogs require communication back from the processor so that
        // they can update their appearance in idle time, and some plug-in
        // libraries (like lv2) require the host program to mediate the
        // communication
        pAccessState->WorkerWrite();
    }

    return result;
}

bool RealtimeEffectState::IsEnabled() const noexcept
{
    return mMainSettings.settings.extra.GetActive();
}

bool RealtimeEffectState::IsActive() const noexcept
{
    return mWorkerSettings.settings.extra.GetActive();
}

void RealtimeEffectState::SetActive(bool active)
{
    auto access = GetAccess();
    access->ModifySettings([&](EffectSettings& settings) {
        settings.extra.SetActive(active);
        return nullptr;
    });
    access->Flush();

    Publish(active
            ? RealtimeEffectStateChange::EffectOn
            : RealtimeEffectStateChange::EffectOff);
}

bool RealtimeEffectState::Finalize() noexcept
{
    mGroups.clear();
    mCurrentProcessor = 0;

    auto pInstance = mwInstance.lock();
    if (!pInstance) {
        return false;
    }

    if (!pInstance->UsesMessages()) {
        // This is the main thread cleaning up a state not now used in processing
        mMainSettings = mWorkerSettings;
    }

    auto result = pInstance->RealtimeFinalize(mMainSettings.settings);
    mLatency = {};
    mInitialized = false;
    return result;
}

const std::string& RealtimeEffectState::XMLTag()
{
    static const std::string result{ "effect" };
    return result;
}

static const auto idAttribute = "id";
static const auto versionAttribute = "version";
static const auto parametersAttribute = "parameters";
static const auto parameterAttribute = "parameter";
static const auto nameAttribute = "name";
static const auto valueAttribute = "value";
static constexpr auto activeAttribute = "active";

bool RealtimeEffectState::HandleXMLTag(
    const std::string_view& tag, const AttributesList& attrs)
{
    if (tag == XMLTag()) {
        mParameters.clear();
        mPlugin = nullptr;
        mID.clear();
        for (auto&[attr, value] : attrs) {
            if (attr == idAttribute) {
                SetID(value.ToWString());
                if (!mPlugin) {
                    // TODO - complain!!!!
                }
            } else if (attr == versionAttribute) {
            } else if (attr == activeAttribute) {
                // Updating the EffectSettingsExtra although we haven't yet built
                // the settings
                mMainSettings.settings.extra.SetActive(value.Get<bool>());
            }
        }
        return true;
    } else if (tag == parametersAttribute) {
        return true;
    } else if (tag == parameterAttribute) {
        wxString n;
        wxString v;
        for (auto&[attr, value] : attrs) {
            if (attr == nameAttribute) {
                n = value.ToWString();
            } else if (attr == valueAttribute) {
                v = value.ToWString();
            }
        }
        mParameters += wxString::Format(wxT("\"%s=%s\" "), n, v);
        return true;
    } else {
        return false;
    }
}

void RealtimeEffectState::HandleXMLEndTag(const std::string_view& tag)
{
    if (tag == XMLTag()) {
        if (mPlugin && !mParameters.empty()) {
            CommandParameters parms(mParameters);
            mPlugin->LoadSettings(parms, mMainSettings.settings);
        }
        mParameters.clear();
    }
}

XMLTagHandler* RealtimeEffectState::HandleXMLChild(const std::string_view& tag)
{
    // Tag may be for the state, or the list of parameters, or for one parameter.
    // See the writing method below.  All are handled by this
    return this;
}

void RealtimeEffectState::WriteXML(XMLWriter& xmlFile)
{
    if (!mPlugin) {
        return;
    }

    xmlFile.StartTag(XMLTag());
    const auto active = mMainSettings.settings.extra.GetActive();
    xmlFile.WriteAttr(activeAttribute, active);
    xmlFile.WriteAttr(idAttribute, PluginManager::GetID(mPlugin));
    xmlFile.WriteAttr(versionAttribute, mPlugin->GetVersion());

    CommandParameters cmdParms;
    if (mPlugin->SaveSettings(mMainSettings.settings, cmdParms)) {
        xmlFile.StartTag(parametersAttribute);

        wxString entryName;
        long entryIndex;
        bool entryKeepGoing;

        entryKeepGoing = cmdParms.GetFirstEntry(entryName, entryIndex);
        while (entryKeepGoing) {
            wxString entryValue = cmdParms.Read(entryName, "");

            xmlFile.StartTag(parameterAttribute);
            xmlFile.WriteAttr(nameAttribute, entryName);
            xmlFile.WriteAttr(valueAttribute, entryValue);
            xmlFile.EndTag(parameterAttribute);

            entryKeepGoing = cmdParms.GetNextEntry(entryName, entryIndex);
        }

        xmlFile.EndTag(parametersAttribute);
    }

    xmlFile.EndTag(XMLTag());
}

std::shared_ptr<EffectSettingsAccess> RealtimeEffectState::GetAccess()
{
    if (!GetEffect()) {
        // Effect not found!
        // Return a dummy
        return std::make_shared<Access>();
    }

    // Only the main thread assigns to the atomic pointer, here and
    // once only in the lifetime of the state
    if (!GetAccessState()) {
        MakeInstance();
        mpAccessState.emplace(*mPlugin, *this);
    }

    return std::make_shared<Access>(*this);
}
